// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';

import 'package:args/args.dart';
import 'package:dartdev/src/commands/compile.dart';
import 'package:dartdev/src/commands/install.dart';
import 'package:dartdev/src/install/file_system.dart';
import 'package:dartdev/src/install/pub_formats.dart';
import 'package:dartdev/src/progress.dart';
import 'package:front_end/src/api_prototype/compiler_options.dart'
    show Verbosity;
import 'package:frontend_server/resident_frontend_server_utils.dart'
    show invokeReplaceCachedDill;
import 'package:path/path.dart';
import 'package:pub/pub.dart';

import '../core.dart';
import '../experiments.dart';
import '../generate_kernel.dart';
import '../native_assets.dart';
import '../resident_frontend_constants.dart';
import '../resident_frontend_utils.dart';
import '../utils.dart';
import '../vm_interop_handler.dart';
import 'compilation_server.dart';

class RunCommand extends DartdevCommand {
  static const String cmdName = 'run';

  static const gitRefOption = 'git-ref';
  static const gitPathOption = 'git-path';

  // kErrorExitCode, as defined in runtime/bin/error_exit.h
  static const errorExitCode = 255;

  // This argument parser is here solely to ensure that VM specific flags are
  // provided before any command and to provide a more consistent help message
  // with the rest of the tool.
  @override
  ArgParser createArgParser() {
    return ArgParser(
      // Don't parse flags after script name.
      allowTrailingOptions: false,
      usageLineLength: dartdevUsageLineLength,
    );
  }

  final bool nativeAssetsExperimentEnabled;
  final bool dataAssetsExperimentEnabled;

  RunCommand({
    bool verbose = false,
    this.nativeAssetsExperimentEnabled = false,
    this.dataAssetsExperimentEnabled = false,
  }) : super(
          cmdName,
          '''Run a Dart program from a file, a local package, or a remote package.

Usage: dart [vm-options] run [arguments] [<dart-file>|<local-package>|<remote-executable> [args]]

<dart-file>
  A path to a Dart script (e.g., `bin/main.dart`).

<local-package>
  An executable from a local package dependency, in the format <package>[:<executable>].
  For example, `test:test` runs the `test` executable from the `test` package.
  If the executable is not specified, the package name is used.

<remote-executable>
  An executable from a remote package. This can be from a hosted package server
  (like pub.dev) or a git repository.

  When running a remote executable, all other command-line flags are disabled,
  except for the options for remote executables. `dart run <remote-executable>`
  uses `dart install` under the hood and compiles the app into a standalone
  executable, preventing passing VM options.

  From a hosted package server:
    <hosted-url>/<package>[@<version>][:<executable>]

    Downloads the package from a hosted package server and runs the specified
    executable.
    If a version is provided, the specified version is downloaded.
    If an executable is not specified, the package name is used.
    For example, `https://pub.dev/dcli@1.0.0:dcli_complete` runs the
    `dcli_complete` executable from version 1.0.0 of the `dcli` package.

  From a git repository:
    <git-url>[:<executable>]

    Clones the git repository and runs the specified executable from it.
    If an executable is not specified, the package name from the cloned
    repository's pubspec.yaml is used.
    The git url can be any valid git url.''',
          verbose,
        ) {
    argParser
      ..addFlag(
        residentOption,
        abbr: 'r',
        negatable: false,
        help: 'Enable faster startup times by using a resident frontend '
            'compiler for compilation.\n'
            'If --$residentCompilerInfoFileOption is provided in conjunction with '
            'this flag, the specified info file will be used, otherwise the '
            'default info file will be used. If there is not already a '
            'compiler associated with the selected info file, one will be '
            "started. Refer to 'dart ${CompilationServerCommand.commandName} "
            "start -h' for more information about info files.",
        hide: !verbose,
      )
      ..addFlag(
        quietOption,
        hide: !verbose,
        help: 'Disable the printing of messages about the resident compiler '
            'starting up / shutting down.',
      )
      ..addOption(
        CompilationServerCommand.residentCompilerInfoFileFlag,
        hide: !verbose,
        help: CompilationServerCommand.residentCompilerInfoFileFlagDescription,
      )
      ..addOption(
        CompilationServerCommand.legacyResidentServerInfoFileFlag,
        // This option is only available for backwards compatibility, and should
        // never be shown in the help message.
        hide: true,
      );
    // NOTE: When updating this list of flags, be sure to add any VM flags to
    // the list of flags in Options::ProcessVMDebuggingOptions in
    // runtime/bin/main_options.cc. Failure to do so will result in those VM
    // options being ignored.
    argParser.addSeparator(
      'Debugging options:',
    );
    argParser
      ..addOption(
        'observe',
        help: 'The observe flag is a convenience flag used to run a program '
            'with a set of common options useful for debugging. '
            'Run `dart help -v run` for details.',
        valueHelp: '[<port>[/<bind-address>]]',
      )
      ..addFlag(
        'enable-asserts',
        help: 'Enable assert statements.',
      )
      ..addOption(
        'launch-dds',
        hide: true,
        help: 'Launch DDS.',
      );

    if (verbose) {
      argParser.addSeparator(
          verbose ? 'Options implied by --observe are currently:' : '');
    }
    argParser
      ..addOption(
        'enable-vm-service',
        help: 'Enables the VM service and listens on the specified port for '
            'connections (default port number is 8181, default bind address '
            'is localhost).',
        valueHelp: '[<port>[/<bind-address>]]',
        hide: !verbose,
      )
      ..addFlag(
        'serve-devtools',
        help: 'Serves an instance of the Dart DevTools debugger and profiler '
            'via the VM service at <vm-service-uri>/devtools.',
        defaultsTo: true,
        hide: !verbose,
      )
      ..addFlag(
        'pause-isolates-on-exit',
        help: 'Pause isolates on exit when '
            'running with --enable-vm-service.',
        hide: !verbose,
      )
      ..addFlag(
        'pause-isolates-on-unhandled-exceptions',
        help: 'Pause isolates when an unhandled exception is encountered '
            'when running with --enable-vm-service.',
        hide: !verbose,
      )
      ..addFlag(
        'warn-on-pause-with-no-debugger',
        help: 'Print a warning when an isolate pauses with no attached debugger'
            ' when running with --enable-vm-service.',
        hide: !verbose,
      )
      ..addOption(
        'timeline-streams',
        help: 'Enables recording for specific timeline streams.\n'
            'Valid streams include: all, API, Compiler, CompilerVerbose, Dart, '
            'Debugger, Embedder, GC, Isolate, Microtask, VM.\n'
            'Defaults to "Compiler, Dart, GC, Microtask" when --observe is '
            'provided.',
        valueHelp: 'str1, str2, ...',
        hide: !verbose,
      );

    if (verbose) {
      argParser.addSeparator('Other debugging options:');
    }
    argParser
      ..addFlag(
        'pause-isolates-on-start',
        help: 'Pause isolates on start when '
            'running with --enable-vm-service.',
        hide: !verbose,
      )
      ..addOption(
        'timeline-recorder',
        help: 'Selects the timeline recorder to use.\n'
            'Valid recorders include: none, ring, endless, startup, '
            'systrace, file, callback, perfettofile.\n'
            'Defaults to ring.',
        valueHelp: 'recorder',
        hide: !verbose,
      )
      ..addFlag(
        'profile-microtasks',
        hide: !verbose,
        negatable: false,
        help: 'Record information about each microtask. Information about '
            'completed microtasks will be written to the "Microtask" '
            'timeline stream.',
      )
      ..addFlag(
        'profile-startup',
        hide: !verbose,
        negatable: false,
        help: 'Make the profiler discard new samples once the profiler '
            'sample buffer is full. When this flag is not set, the '
            'profiler sample buffer is used as a ring buffer, meaning that '
            'once it is full, new samples start overwriting the oldest '
            'ones. This flag itself does not enable the profiler; the '
            'profiler must be enabled separately, e.g. with --profiler.',
      )
      ..addSeparator('Logging options:')
      ..addOption(
        'verbosity',
        help: 'Sets the verbosity level of the compilation.',
        defaultsTo: Verbosity.defaultValue,
        allowed: Verbosity.allowedValues,
        allowedHelp: Verbosity.allowedValuesHelp,
      );

    if (verbose) {
      argParser.addSeparator('Advanced options:');
    }
    argParser
      ..addMultiOption(
        'define',
        abbr: 'D',
        valueHelp: 'key=value',
        help: 'Define an environment declaration.',
        hide: !verbose,
      )
      ..addFlag(
        'disable-service-auth-codes',
        hide: !verbose,
        negatable: false,
        help: 'Disables the requirement for an authentication code to '
            'communicate with the VM service. Authentication codes help '
            'protect against CSRF attacks, so it is not recommended to '
            'disable them unless behind a firewall on a secure device.',
      )
      ..addFlag(
        'enable-service-port-fallback',
        hide: !verbose,
        negatable: false,
        help: 'When the VM service is told to bind to a particular port, '
            'fallback to 0 if it fails to bind instead of failing to '
            'start.',
      )
      ..addOption(
        'namespace',
        hide: !verbose,
        valueHelp: 'path',
        help: 'The path to a directory that dart:io calls will treat as the '
            'root of the filesystem.',
      )
      ..addOption(
        'root-certs-file',
        hide: !verbose,
        valueHelp: 'path',
        help: 'The path to a file containing the trusted root certificates '
            'to use for secure socket connections.',
      )
      ..addOption(
        'root-certs-cache',
        hide: !verbose,
        valueHelp: 'path',
        help: 'The path to a cache directory containing the trusted root '
            'certificates to use for secure socket connections.',
      )
      ..addFlag(
        'trace-loading',
        hide: !verbose,
        negatable: false,
        help: 'Enables tracing of library and script loading.',
      )
      ..addOption(
        'packages',
        hide: !verbose,
        valueHelp: 'path',
        help: 'The path to the package resolution configuration file, which '
            'supplies a mapping of package names\ninto paths.',
      )
      ..addOption(
        'write-service-info',
        help: 'Outputs information necessary to connect to the VM service to '
            'specified file in JSON format. Useful for clients which are '
            'unable to listen to stdout for the Dart VM service listening '
            'message.',
        valueHelp: 'file',
        hide: !verbose,
      )
      ..addFlag('dds',
          hide: !verbose,
          help: 'Use the Dart Development Service (DDS) for enhanced debugging '
              'functionality. Note: Disabling DDS may break some '
              'functionality in IDEs and other tooling.',
          defaultsTo: true)
      ..addFlag('serve-observatory',
          hide: !verbose,
          help: 'Enable hosting Observatory through the VM Service.',
          defaultsTo: true)
      ..addFlag(
        'print-dtd',
        hide: !verbose,
        help: 'Prints connection details for the Dart Tooling Daemon (DTD).'
            'Useful for Dart DevTools extension authors working with DTD in the '
            'extension development environment.',
      )
      ..addFlag(
        'debug-dds',
        hide: true,
      )
      ..addExperimentalFlags(verbose: verbose)
      ..addSeparator('Options for remote executables:')
      ..addOption(
        gitPathOption,
        help: 'Path of git package in repository. '
            'Only applies when using a git url for <remote-executable>.',
      )
      ..addOption(
        gitRefOption,
        help: 'Git branch or commit to be retrieved. '
            'Only applies when using a git url for <remote-executable>.',
      );
  }

  @override
  String get invocation =>
      '${super.invocation} [<dart-file|package-target> [args]]';

  @override
  CommandCategory get commandCategory => CommandCategory.project;

  /// Attempts to compile [executable] to a kernel file using the Resident
  /// Frontend Compiler associated with [residentCompilerInfoFile]. If
  /// [shouldRetryOnFrontendCompilerException] is true, when a
  /// [FrontendCompilerException] is encountered during compilation, the
  /// Resident Frontend Compiler will be restarted, and compilation will be
  /// retried. This method returns the compiled kernel file if compilation
  /// succeeds, otherwise it returns null.
  static Future<DartExecutableWithPackageConfig?>
      _compileToKernelUsingResidentCompiler({
    required DartExecutableWithPackageConfig executable,
    required File residentCompilerInfoFile,
    required ArgResults args,
    required bool shouldRetryOnFrontendCompilerException,
    required bool quiet,
  }) async {
    final executableFile = File(executable.executable);
    assert(!await isFileKernelFile(executableFile) &&
        !await isFileAppJitSnapshot(executableFile) &&
        !await isFileAotSnapshot(executableFile));

    try {
      return await generateKernel(
        executable,
        residentCompilerInfoFile,
        args,
        createCompileJitJson,
        quiet: quiet,
      );
    } on FrontendCompilerException catch (e) {
      if (e.issue == CompilationIssue.serverError) {
        if (shouldRetryOnFrontendCompilerException) {
          if (!quiet) {
            log.stderr(
              'Error: A connection to the Resident Frontend Compiler could '
              'not be established. Restarting the Resident Frontend Compiler '
              'and retrying compilation.',
            );
          }
          await shutDownOrForgetResidentFrontendCompiler(
            residentCompilerInfoFile,
          );
          return _compileToKernelUsingResidentCompiler(
            executable: executable,
            residentCompilerInfoFile: residentCompilerInfoFile,
            args: args,
            shouldRetryOnFrontendCompilerException: false,
            quiet: quiet,
          );
        } else {
          log.stderr(
            'Error: A connection to the Resident Frontend Compiler could '
            "not be established. Please re-run 'dart run --$residentOption' and a "
            'new compiler will automatically be started in its place.',
          );
          await shutDownOrForgetResidentFrontendCompiler(
            residentCompilerInfoFile,
          );
          return null;
        }
      } else {
        log.stderr(
            '${ansi.yellow}Failed to build ${executable.executable}:${ansi.none}');
        log.stderr(e.message);
        return null;
      }
    }
  }

  @override
  FutureOr<int> run() async {
    final args = argResults!;
    var mainCommand = '';
    var runArgs = <String>[];
    if (args.rest.isNotEmpty) {
      mainCommand = args.rest.first;
      // The command line arguments after the command name.
      runArgs = args.rest.skip(1).toList();
    }

    if (_isRemoteRun(mainCommand)) {
      return _runRemote(args, mainCommand, runArgs);
    }
    return _runLocal(args, mainCommand, runArgs);
  }

  FutureOr<int> _runLocal(
    ArgResults args,
    String mainCommand,
    List<String> runArgs,
  ) async {
    final String? residentCompilerInfoFileArg =
        args[CompilationServerCommand.residentCompilerInfoFileFlag] ??
            args[CompilationServerCommand.legacyResidentServerInfoFileFlag];
    final useResidentCompiler = args.wasParsed(residentOption);
    if (residentCompilerInfoFileArg != null && !useResidentCompiler) {
      log.stderr(
        'Error: the --$residentOption flag must be passed whenever the '
        '--$residentCompilerInfoFileOption option is passed.',
      );
      return errorExitCode;
    }
    if (args.wasParsed(quietOption) && !useResidentCompiler) {
      log.stderr(
        'Error: the --$residentOption flag must be passed whenever the '
        '--$quietOption flag is passed.',
      );
      return errorExitCode;
    }
    if (args.wasParsed(gitPathOption) || args.wasParsed(gitRefOption)) {
      usageException(
        'Options `--$gitPathOption` and `--$gitRefOption` '
        'can only be used with a remote executable.',
      );
    }

    String? nativeAssets;
    final packageConfigUri = await DartNativeAssetsBuilder.ensurePackageConfig(
      Directory.current.uri,
    );
    if (packageConfigUri != null) {
      final packageConfig =
          await DartNativeAssetsBuilder.loadPackageConfig(packageConfigUri);
      if (packageConfig == null) {
        return compileErrorExitCode;
      }
      final runPackageName = getPackageForCommand(mainCommand) ??
          await DartNativeAssetsBuilder.findRootPackageName(
            Directory.current.uri,
          );
      if (runPackageName != null) {
        final pubspecUri = await DartNativeAssetsBuilder.findWorkspacePubspec(
            packageConfigUri);
        final builder = DartNativeAssetsBuilder(
          pubspecUri: pubspecUri,
          packageConfigUri: packageConfigUri,
          packageConfig: packageConfig,
          runPackageName: runPackageName,
          // Enable accessing assets of dev dependencies in the debugger and
          // enabling commands such as `dart run test` and `dart run
          // coverage_with_test` that rely on having dev dependencies.
          includeDevDependencies: true,
          verbose: verbose,
          dataAssetsExperimentEnabled: dataAssetsExperimentEnabled,
        );
        if (!nativeAssetsExperimentEnabled) {
          if (await builder.warnOnNativeAssets()) {
            return errorExitCode;
          }
        } else if (await builder.hasHooks()) {
          final verbosity = args.option('verbosity')!;
          final showProgress = verbosity != Verbosity.error.name;
          final assetsYamlFileUri = await (showProgress
              ? progress(
                  'Running build hooks',
                  builder.compileNativeAssetsJitYamlFile,
                )
              : builder.compileNativeAssetsJitYamlFile());
          if (assetsYamlFileUri == null) {
            log.stderr('Error: Running build hooks failed.');
            return errorExitCode;
          }
          nativeAssets = assetsYamlFileUri.toFilePath();
        }
      }
    }

    DartExecutableWithPackageConfig executable;
    final hasExperiments = args.enabledExperiments.isNotEmpty;
    try {
      executable = await getExecutableForCommand(
        mainCommand,
        allowSnapshot: !(useResidentCompiler || hasExperiments),
        nativeAssets: nativeAssets,
      );
    } on CommandResolutionFailedException catch (e) {
      log.stderr(e.message);
      return errorExitCode;
    }

    if (useResidentCompiler) {
      final File? residentCompilerInfoFile =
          getResidentCompilerInfoFileConsideringArgs(args);
      if (residentCompilerInfoFile == null) {
        log.stderr(
          CompilationServerCommand
              .inaccessibleDefaultResidentCompilerInfoFileMessage,
        );
        return errorExitCode;
      }

      // Ensure the parent directory exists.
      if (!residentCompilerInfoFile.parent.existsSync()) {
        residentCompilerInfoFile.parent.createSync();
      }

      final executableFile = File(executable.executable);
      if (await isFileKernelFile(executableFile)) {
        // If the file is a kernel file, we do not need to compile it, but we do
        // need to replace the file in the resident frontend compiler kernel
        // cache associated with this executable, because the cached kernel file
        // may be used to populate context for expression evaluation later.
        await ensureCompilationServerIsRunning(
          residentCompilerInfoFile,
          quiet: args[quietOption] ?? false,
        );
        final succeeded = await invokeReplaceCachedDill(
          replacementDillPath: executableFile.absolute.path,
          serverInfoFile: residentCompilerInfoFile,
        );
        if (!succeeded) {
          log.stderr(
            'Error: Encountered a problem accessing the Resident Frontend '
            "Compiler's kernel file cache. Please try re-running the same "
            'command again. If the error persists, please file an issue at '
            'https://github.com/dart-lang/sdk/issues/new.',
          );
          return errorExitCode;
        }
      } else if (!await isFileAppJitSnapshot(executableFile) &&
          !await isFileAotSnapshot(executableFile)) {
        final compiledKernelFile = await _compileToKernelUsingResidentCompiler(
          executable: executable,
          residentCompilerInfoFile: residentCompilerInfoFile,
          args: args,
          shouldRetryOnFrontendCompilerException: true,
          quiet: args[quietOption] ?? false,
        );
        if (compiledKernelFile == null) {
          return errorExitCode;
        } else {
          executable = compiledKernelFile;
        }
      }
    }

    VmInteropHandler.run(
      executable.executable,
      runArgs,
      packageConfigOverride:
          args.option('packages') ?? executable.packageConfig,
      useExecProcess: true,
    );
    return 0;
  }

  static RemoteSourceKind? _remoteSourceKindFromArgument(String argument) {
    if (argument.startsWith('git@')) {
      return RemoteSourceKind.git;
    }
    final potentialUri =
        argument.split(_colonButNoSlashes).first.split('@').first;
    final endsWithDotGitRegex = RegExp(r'\.git[/\\]?$');
    if (endsWithDotGitRegex.hasMatch(potentialUri)) {
      return RemoteSourceKind.git;
    }
    final parsedUri = Uri.tryParse(potentialUri);
    if (parsedUri != null) {
      switch (parsedUri.scheme.toLowerCase()) {
        case 'git':
          return RemoteSourceKind.git;
        case 'http':
        case 'https':
          return RemoteSourceKind.hosted;
      }
    }
    final parsedGitSshUrl = GitSshUrl.tryParse(potentialUri);
    if (parsedGitSshUrl != null) {
      return RemoteSourceKind.git;
    }

    // Local execution.
    return null;
  }

  static bool _isRemoteRun(String mainCommand) {
    return _remoteSourceKindFromArgument(mainCommand) != null;
  }

  /// Parse the arguments for remote run.
  ///
  /// Constructs a [InstallCommandParsedArguments] to be able to reuse the
  /// [InstallCommand] implementation.
  InstallCommandParsedArguments _parseRemoteArguments(String mainCommand) {
    final argResults = this.argResults!;

    final sourceKind = _remoteSourceKindFromArgument(mainCommand)!;

    final gitPath = argResults.option(gitPathOption);
    var gitRef = argResults.option(gitRefOption);
    if (sourceKind != RemoteSourceKind.git &&
        (gitPath != null || gitRef != null)) {
      usageException(
        'Options `--$gitPathOption` and `--$gitRefOption` '
        'can only be used with a git source.',
      );
    }

    for (final option in argResults.options) {
      if (argResults.wasParsed(option) &&
          option != gitPathOption &&
          option != gitRefOption &&
          option != verbosityOption) {
        usageException(
          'Option $option cannot be used in remote runs. '
          '`dart run <remote-executable>` uses `dart install` under the hood '
          'and compiles the app into a standalone executable.',
        );
      }
    }

    String? hostedUrl;
    String? versionConstraint;
    final String source;
    switch (sourceKind) {
      case RemoteSourceKind.git:
        if (mainCommand.startsWith('git@') && mainCommand.contains('.git')) {
          // Valid values might contain a colon for the command or not:
          // - git@github.com:org/repo.git
          // - git@github.com:org/repo.git:executable
          // Drop everything after the 2nd colon for the git repository.
          source = mainCommand.split(':').sublist(0, 2).join(':');
        } else {
          source = mainCommand.split(_colonButNoSlashes).first;
        }
      case RemoteSourceKind.hosted:
        final parsedUri = Uri.parse(
            mainCommand.split('@').first.split(_colonButNoSlashes).first);
        hostedUrl = '${parsedUri.scheme}://${parsedUri.host}';
        source = parsedUri.path.replaceFirst('/', '');
        versionConstraint = mainCommand
                .split('@')
                .lastButNotFirstOrNull
                ?.split(_colonButNoSlashes)
                .first ??
            'any';
      case RemoteSourceKind.path:
        throw StateError('Unreachable');
    }

    return InstallCommandParsedArguments(
      source: source,
      sourceKind: sourceKind,
      versionConstraint: versionConstraint,
      gitPath: gitPath,
      gitRef: gitRef,
      hostedUrl: hostedUrl,
      overwrite: false,
    );
  }

  Future<String> _findPackageName(
    InstallCommandParsedArguments parsedArgs,
  ) async {
    switch (parsedArgs.sourceKind) {
      case RemoteSourceKind.git:
        return await getPackageNameFromGitRepo(
          parsedArgs.source,
          ref: parsedArgs.gitRef,
          path: parsedArgs.gitPath,
          relativeTo: Directory.current.path,
          tagPattern: null,
        );
      case RemoteSourceKind.hosted:
        return parsedArgs.source;

      case RemoteSourceKind.path:
        throw StateError('Unreachable');
    }
  }

  /// Installs (if needed) and runs the remote executable.
  ///
  /// Installs the app bundle at the same location as `dart install` but does
  /// not symlink the executable.
  Future<int> _runRemote(
    ArgResults args,
    String mainCommand,
    List<String> runArgs,
  ) async {
    final parsedArgs = _parseRemoteArguments(mainCommand);
    final packageName = await _findPackageName(parsedArgs);

    return await InstallCommand.inTempDir((tempDirectory) async {
      try {
        // Create a helper package for running a pub-resolve and pulling in the
        // wanted package and its dependencies.
        final helperPackageDirectory =
            Directory.fromUri(tempDirectory.uri.resolve('helperPackage/'));
        helperPackageDirectory.createSync();
        InstallCommand.createHelperPackagePubspec(
          helperPackageDir: helperPackageDirectory,
          packageName: packageName,
          parsedArgs: parsedArgs,
        );
        await InstallCommand.resolveHelperPackage(helperPackageDirectory);
        final helperPackageLockFile =
            File.fromUri(helperPackageDirectory.uri.resolve('pubspec.lock'));

        final appBundleDirectory = InstallCommand.selectAppBundleDirectory(
          parsedArgs,
          packageName,
          helperPackageDirectory,
          helperPackageLockFile,
        );

        // If the pubspec lock file changed, re-build the executable.
        if (!appBundleDirectory
            .pubspecLockIsIdenticalTo(helperPackageLockFile)) {
          final helperPackageConfigFile = File.fromUri(helperPackageDirectory
              .uri
              .resolve('.dart_tool/package_config.json'));

          final sourcePackageRootDirectory = Directory(Uri.parse(
            PackageConfigFile.loadSync(helperPackageConfigFile)
                .packages
                .firstWhere((e) => e.name == packageName)
                .rootUri,
          ).toFilePath())
              .ensureEndWithSeparator;

          final sourcePackagePubspecFile = File.fromUri(
              sourcePackageRootDirectory.uri.resolve('pubspec.yaml'));

          final executables = InstallCommand.loadDeclaredExecutables(
            sourcePackagePubspecFile,
            sourcePackageRootDirectory,
          );

          final buildDirectory =
              Directory.fromUri(tempDirectory.uri.resolve('build/'));
          final verbosity = args.option('verbosity')!;
          await InstallCommand.doBuild(
            executables,
            buildDirectory,
            helperPackageConfigFile,
            sourcePackagePubspecFile,
            verbose,
            verbosity,
          );

          await InstallCommand.createAppBundleDirectory(
            appBundleDirectory,
            buildDirectory,
            helperPackageLockFile,
            sourcePackagePubspecFile,
          );
        }

        final mainCommandRemainder =
            mainCommand.substring(parsedArgs.source.length);
        final executable = mainCommandRemainder
                .split(_colonButNoSlashes)
                .lastButNotFirstOrNull ??
            packageName;
        final executableUri =
            appBundleDirectory.directory.uri.resolve('bundle/bin/$executable');
        final arguments = args.rest.skip(1).toList();

        // The app-bundle contains executables (not AOT snapshots) to make it
        // self-contained. So, spawn a process instead of loading a snapshot in
        // the VM.
        final process = await Process.start(
          executableUri.toFilePath(),
          arguments,
          mode: ProcessStartMode.inheritStdio, // Enable using stdin etc.
        );
        return await process.exitCode;
      } on InstallException catch (e) {
        stderr.writeln(e.message);
        return genericErrorExitCode;
      }
    });
  }
}

extension<T> on List<T> {
  /// Return the last element, but only if there are at least two elements.
  T? get lastButNotFirstOrNull {
    if (length < 2) return null;
    return last;
  }
}

/// Does not match the :// in an url scheme or the :\ in a Windows path.
final _colonButNoSlashes = RegExp(r':(?!(//|\\))');

/// Keep in sync with [getExecutableForCommand].
///
/// Returns `null` if root package should be used.
// TODO(https://github.com/dart-lang/pub/issues/4067): Don't duplicate logic.
String? getPackageForCommand(String descriptor) {
  final root = current;
  var asPath = descriptor;
  try {
    asPath = Uri.parse(descriptor).toFilePath();
  } catch (_) {
    /// Here to get the same logic as[getExecutableForCommand].
  }
  final asDirectFile = join(root, asPath);
  if (File(asDirectFile).existsSync()) {
    return null; // root package.
  }
  if (!File(join(root, 'pubspec.yaml')).existsSync()) {
    return null;
  }
  String package;
  if (descriptor.contains(':')) {
    final parts = descriptor.split(':');
    if (parts.length > 2) {
      return null;
    }
    package = parts[0];
    if (package.isEmpty) {
      return null; // root package.
    }
  } else {
    package = descriptor;
    if (package.isEmpty) {
      return null; // root package.
    }
  }
  if (package == 'test') {
    // `dart run test` is expected to behave as `dart test`.
    return null; // root package.
  }
  return package;
}
